#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2020/5/20 19:43
# @Author : StableGenius
# @Site : https://www.zhihu.com/people/im401
# @File : check_paper_status.py
# @Software: PyCharm
# @Description: Check paper status

import re
import smtplib
from email.mime.text import MIMEText
import os
from typing import Optional
from io import BytesIO
import time

import requests
import execjs
from urllib.parse import urlparse
from fake_useragent import UserAgent
from PIL import Image
import pytesseract


class MyMail:
    """
    使用这个类来处理发邮件相关的设置，比如自己的用户名和密码
    """
    def __init__(self, mail_host, mail_user, mail_pass, sender, receiver, title):
        self.mail_host = mail_host  # SMTP服务器
        self.mail_user = mail_user  # 用户名
        self.mail_pass = mail_pass  # 授权密码，非登录密码

        self.sender = sender    # 发件人邮箱(最好写全, 不然会失败)
        self.receiver = receiver  # 接收邮件，可设置为你的QQ邮箱或者其他邮箱

        self.content = ''  # 邮件的正文内容
        self.title = title  # 邮件主题

    def send_email(self):
        message = MIMEText(self.content, 'plain', 'utf-8')  # 内容, 格式, 编码
        message['From'] = self.sender
        message['To'] = self.receiver
        message['Subject'] = self.title

        try:
            smtp_obj = smtplib.SMTP_SSL(self.mail_host, 465)  # 启用SSL发信, 端口一般是465
            smtp_obj.login(self.mail_user, self.mail_pass)  # 登录验证
            smtp_obj.sendmail(self.sender, self.receiver, message.as_string())  # 发送
            print("mail has been send successfully.")
        except smtplib.SMTPException as e:
            print(e)


class Paper:
    """
    使用这个类来获取论文状态
    """
    def __init__(self, login_url: str, post_url: str, captcha_url: Optional[str] = None):
        """
        根据投稿系统的相关信息进行配置
        :param login_url: 投稿系统的登录网址
        :param post_url: 登录系统时发送POST请求的目标网址
        :param captcha_url: 获取验证码的目标网址，留空表示不需要验证码
        """
        self.login_url = login_url
        self.post_url = post_url
        self.captcha_url = captcha_url

        # 使用session管理登录状态，这里只是放一个占位符防止出现警告而已
        self.session = None

        # 切割登录链接，填充headers
        self.host = urlparse(self.login_url).netloc
        self.headers = {
            'Host': self.host,
            'User-Agent': str(UserAgent().random),
        }

        # 读取加密用的js代码
        file_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "encdec.js")
        with open(file_path) as f:
            self.js = execjs.compile(f.read())

        # 登录时提交的表单内容
        self.form_data = {}

        # 稿件的初始状态
        self.status = "外审"

    def login(self, username: str, password: str) -> None:
        """
        一直尝试登录，直到成功为止
        :param username: 用户名
        :param password: 密码
        :return:
        """
        # 每次登陆的时候都需要重新设置一下session，因为时间间隔比较长，旧cookie其实是失效了的
        self.session = requests.Session()

        # 打开登录页面，获取基本的cookie
        self.session.get(url=self.login_url, headers=self.headers)
        self.session.get(url='http://tjyj.stats.gov.cn/favicon.ico', headers=self.headers)

        # 根据用户名和密码，计算出必要的加密字符串
        # 加密用户名和密码
        self.form_data = {
            'j_username': self.js.call("des", username, "ech", "gt", "ma") + '___1___author___jaiimjlsuan',
            'j_password': self.js.call("des", password, "ech", "gt", "ma"),
        }

        # 更新session的cookies
        self.session.cookies.set('author_password_id', '', domain=self.host)
        self.session.cookies.set('author_user_id', self.js.call('enc_cookie', username), domain=self.host)

        # 尝试正确识别验证码，两秒一次
        while not self._login():
            print("当前尝试登录不成功，休息两秒后继续尝试！")
            time.sleep(2)

    def _login(self) -> bool:
        # 如果需要获取验证码的话，就去拿一张验证码
        if self.captcha_url:
            img = self.session.get(url=self.captcha_url, headers=self.headers).content
            captcha_code = self._handle_captcha(img)

            if captcha_code:
                print("识别结果为 ", captcha_code, " ，对不对我不知道，反正你就说快不快吧！")
                self.form_data.update({
                    'j_randomCode': captcha_code
                })
            else:
                return False

        # 发送POST请求的时候要更新一下headers
        headers = self.headers.copy()
        headers.update({
            'Content-Type': 'application/x-www-form-urlencoded',
            'Origin': 'http://tjyj.stats.gov.cn',
            'Referer': 'http://tjyj.stats.gov.cn/JournalX_tjyj/authorLogOn.action',
        })

        before_cookies = self.session.cookies.values()
        # 向服务器发送POST请求，模拟登录
        print("尝试模拟登录中...")
        self.session.post(url=self.post_url,
                          headers=headers,
                          data=self.form_data,
                          allow_redirects=False)

        for value in self.session.cookies.values():
            # 只有session的cookie发生了变化才算是登录成功
            if value not in before_cookies:
                return True

        # cookie没有发生变化就是登陆失败，包括了密码错误，验证码错误，500错误等等所有情况
        return False

    @classmethod
    def _handle_captcha(cls, io_content) -> str:
        """
        使用ocr识别验证码
        :param io_content: 输入的IO流
        :return: 返回识别后的结果
        """
        # 从IO流中读取图片
        img = Image.open(BytesIO(io_content))

        # 剪掉周围一圈黑边
        size = img.size
        img = img.crop((0.06*size[0], 0.06*size[1], 0.94*size[0], 0.94*size[1]))  # left, upper, right, lower

        # 灰度及二值化处理，增大对比度
        img = img.convert('L')  # 灰度处理

        threshold = 90  # 二值化阈值
        table = []
        for i in range(256):
            x = 0 if i < threshold else 1
            table.append(x)

        img = img.point(table, '1')
        result = pytesseract.image_to_string(img, config='--psm 6 tessedit_char_whitelist = 0123456789')
        # 只有result是五个数字的时候才认为是成功地识别了验证码
        result = ''.join(re.findall(r'\d+', result))
        if len(result) == 5:
            return result
        else:
            print(f"{result} OCR识别不太靠谱啊...")
            return None

    def check_status(self, status_url: str):
        """
        查阅稿件状态
        :param status_url: 查询稿件状态的url
        :return:
        """
        resp = self.session.get(url=status_url, headers=self.headers).text
        if status := re.search(r'状态.*?([\u4e00-\u9fa5]+)', resp, re.S):
            print('稿件状态查询成功！')
            if self.status != status.group(1):
                print("稿件状态出现更新，应使用邮件通知老铁！")
                self.status = status.group(1)
                return True
            else:
                # 成功查询到稿件状态，但稿件状态未更新
                return False
        else:
            print("稿件状态查询失败！")
            return None


def run(config_dict: dict) -> None:
    """
    程序入口
    :param config_dict: 填入配置字典
    :return:
    """
    # 配置登录前的准备信息
    paper = Paper(login_url=config_dict['login_url'],
                  post_url=config_dict['post_url'],
                  captcha_url=config_dict.get('captcha_url', ''))

    # 配置邮箱
    my_mail = MyMail(mail_host=config_dict['host'],
                     mail_user=config_dict['mail_user'],
                     mail_pass=config_dict['mail_pass'],
                     sender=config_dict['sender'],
                     receiver=config_dict['receiver'],
                     title=config_dict['title'])

    while True:
        # 输入用户名和密码，尝试登录
        paper.login(config_dict['username'], config_dict['password'])

        # 成功登录后获取稿件状态
        update_flag = paper.check_status(config_dict['status_url'])

        if update_flag:
            print('尝试给我自己发邮件！')
            my_mail.content = f'您好！您所尝试登录的网站{config_dict["login_url"]}上的稿件状态出现了更新，它更新后的状态为{paper.status}，请速速登录网站查看详情吧！'
            my_mail.send_email()
            time.sleep(8*60*60)
        elif update_flag is None:
            print('五分钟以后重新运行脚本！')
            time.sleep(5*60)
        else:
            print('八小时以后再重新检查稿件状态！')
            time.sleep(8*60*60)


if __name__ == '__main__':
    # 配置相关信息
    config = {
        'login_url': "http://tjyj.stats.gov.cn/JournalX_tjyj/authorLogOn.action",
        'post_url': "http://tjyj.stats.gov.cn/JournalX_tjyj/j_acegi_security_check",
        'captcha_url': 'http://tjyj.stats.gov.cn/JournalX_tjyj/kaptcha.jpg',
        "status_url": "http://tjyj.stats.gov.cn/JournalX_tjyj/author/Author!disposing.action",
        'username': '',
        'password': '',
        'host': 'smtp.163.com',
        'mail_user': '',
        'mail_pass': '',
        'sender': '',
        'receiver': '',
        'title': '稿件状态出现更新'
    }

    # 根据配置信息，启动程序
    run(config_dict=config)
